【原创】TCP协议Nagle算法和Delayed ACK相互影响实例分析
建议:阅读本文之前,最好对于TCP的发送、重发以及ACK机制有所了解。
问题描述
最近在一个消息中间件系统(该消息中间件由客户端SDK和服务端Server组成)的性能测试时,发现每个请求的响应时间大概在40ms-50ms之间,这明显过大了。最终定位,是因为SDK没有禁用TCP的Nagle算法导致的。但其根本原理是因为TCP的Delayed Ack机制和Nagle Algorithm相互影响导致的。
概念详解
TCP-Delayed Ack
目的:用于防止只发送一个单独的Ack包,而是希望发送的包是一个Ack+一份数据组成一个包,这样能较少交互次数,减少网络资源消耗。这个设定是基于对于一般交互场景的一个基本假设:数据接收方会生成一个响应给数据发送方。
原理:当数据接收方收到一个TCP包之后,先不回应ACK包,而是等待一定的时间(本例中是40ms),直到
- 数据接收方发送一份响应数据
当数据接收方,收到足够的数据,进行业务处理后,一般会返回业务响应,这时会立即返回:业务响应+ACK。- 接收到连续的TCP包
当服务接收端接收到1号TCP包后,会先延迟发送1号包的ACK,等待2号包到来,2号包到达时,则立即返回一次ACK;当3号包到来时,会延迟3号包的ACK,等待4号包到来,4号包到达时,则立即返回一次ACK……
那么,这样就造成偶数序号的TCP包到达的时候,就会立即返回一次ACK;而奇数序号的包到达时,则延迟ACK响应,等待后续的偶数包到来。- 超时
超过40ms。TCP-Nagle Algorithm
目的:用于防止发送大量的小包,降低网络资源消耗。
原理:当数据发送方写入TCP缓冲区的数据小于MSS(最大报文长度),则暂不发送,等待写入的数据达到MSS再发送;除非在等待的过程中,发送端发送出去的所有TCP报文均已被ACK,这样就可以不用等待写入数据达到MSS,直接发送出去了。我们称没有达到MSS的报文为小包的话,那么其实TCP-Nagle Algorithm就保证了一个连接在一个时刻,有且只能有一个没有被确认的小包。
关于以上两点,可以参考这篇文章,介绍的很详细:http://www.stuartcheshire.org/papers/NagleDelayedAck/
过程分析
该消息中间件系统在做性能测试时,SDK没有禁用Nagle算法,而Server端禁用了;测试时的消息长度为消息头(12)+消息体(14)Byte,TCP MSS是1460Byte;服务端和SDK交互采用类似TCP三次握手的确认机制来保证高可靠性。那么考虑如下过程:
- SDK端发送第一条消息,写入了TCP Buffer,虽然未达到MSS,但是因为没有需要确认的包,所以会立即发送;
- Server端收到TCP包后,就延迟ACK响应;同时SDK端由于启用了Nagle算法,并且存在没有ACK的包,因此处于等待中;
- Server端业务层解包进行业务处理,处理完成后,立即发送业务响应,并捎带ACK返回给SDK;
- SDK端收到了Server端返回的响应和ACK后,立即回复业务确认消息,并捎带ACK;
- 由于Server端收到业务确认消息后,不用再返回响应给SDK,因此延迟ACK确认;
- SDK端发送第二条消息,但是因为启用了Nagle算法,所以必须等待ACK。直到第5步的延迟确认到达,则立即发送第二条消息。
Tcpdump抓包如下:
1 2015-05-25 18:42:49.736897 IP 100.84.52.90.58237 > 100.84.73.45.9090: P 71:101(30) ack 20 win 16420 E..FU3@.=..OdT4ZdTI-.}#...(.R.mpP.@$.).......).... queueAThis is test 2015-05-25 18:42:49.737763 IP 100.84.73.45.9090 > 100.84.52.90.58237: P 20:39(19) ack 101 win 46 E..;Fn@.@...dTI-dT4Z#..}R.mp..) P...F].......*......SUCCESS 2015-05-25 18:42:49.744738 IP 100.84.52.90.58237 > 100.84.73.45.9090: P 101:113(12) ack 39 win 16415 E..4U4@.=..`dT4ZdTI-.}#...) R.m.P.@.d........4...... 2015-05-25 18:42:49.784810 IP 100.84.73.45.9090 > 100.84.52.90.58237: . ack 113 win 46 E..(Fo@.@..1dTI-dT4Z#..}R.m...).P....... 2015-05-25 18:42:49.785438 IP 100.84.52.90.58237 > 100.84.73.45.9090: P 113:143(30) ack 39 win 16415 E..FU5@.=..MdT4ZdTI-.}#...).R.m.P.@..........).... queueAThis is test 2015-05-25 18:42:49.785450 IP 100.84.73.45.9090 > 100.84.52.90.58237: . ack 143 win 46 E..(Fp@.@..0dTI-dT4Z#..}R.m...)3P....... 2015-05-25 18:42:49.786147 IP 100.84.73.45.9090 > 100.84.52.90.58237: P 39:58(19) ack 143 win 46 E..;Fq@.@...dTI-dT4Z#..}R.m...)3P...F].......*......SUCCESS 2015-05-25 18:42:49.790470 IP 100.84.52.90.58237 > 100.84.73.45.9090: P 143:173(30) ack 58 win 16410 E..(Fo@.@..1dTI-dT4Z#..}R.m...).P.......总结:通过以上过程可以发现,由于第5步的延迟ACK,导致SDK发送消息延迟,最终导致了性能下降。如果SDK端关闭的Nagle算法,SDK就可以立即发送第二条消息,而不会受到Nagle算法的约束,一直等待第5步的ACK到达。
代码说明
在Netty中,可以通过设置tcpNoDelay选项,开启或者禁用Nagle算法。
1
2
3
4
5
6
7
8 // 用来禁用TCP的Nagle算法。
bootstrap.setOption("tcpNoDelay", true);
// Netty底层其实就是使用JDK中的Socket.class的setTcpNoDelay方法来设置。
/**
* Enable/disable TCP_NODELAY (disable/enable Nagle's algorithm).
*/
public void setTcpNoDelay(boolean on) throws SocketException;